Zum Hauptinhalt springen

Übung 2 — Die Lernschleife

Was du in dieser Übung baust: Du erweiterst den Assistenten aus Übung 1 so, dass er aus Rückfragen und Korrekturen lernt. Am Ende versteht er eine Formulierung, die er anfangs falsch zugeordnet hat, beim nächsten Mal sofort richtig — ohne KI.

Was sich ändert​

BausteinÄnderung
Tabellenneu: assistent_lernbeispiel
einheitsformatbildet zusätzlich das Muster der Nachricht
kontext_ladenlädt gelernte Treffer, Lernbeispiele und den letzten Vorgang
ki_erkennenbekommt ähnliche Lernbeispiele und liefert Alternativen
entscheidenfünf neue Regeln: Auswahl, Korrektur, gelernter Treffer, „was hast du gelernt?“, „vergiss …“
neu: r_lernenspeichert Lernbeispiele
Skills und rueckkanalreichen einen Hinweis „Gemerkt: …“ durch

Schritt 1 — Die Tabelle​

CREATE TABLE assistent_lernbeispiel (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, muster TEXT, falsch TEXT, richtig TEXT, absender_id TEXT, vorgang_id TEXT, treffer INTEGER DEFAULT 0, aktiv INTEGER DEFAULT 1, erstellt TEXT DEFAULT (datetime('now')));

In dieser Übung lernt der Assistent pro Person: Ein Lernbeispiel gilt nur für die absender_id, die es erzeugt hat.

Schritt 2 — Das Muster bilden​

Füge in einheitsformat diese Funktion ein:

function muster(t) {
t = ' ' + String(t || '').toLowerCase() + ' ';
t = t.replace(/[’']s(?= )/g, ' es').replace(/[^a-zäöüß0-9 ]+/g, ' ');
function ersetze(s, liste, platz) { return s.replace(new RegExp(' (' + liste + ')(?= )', 'g'), ' ' + platz); }
t = ersetze(t, 'januar|februar|märz|april|mai|juni|juli|august|september|oktober|november|dezember', '<monat>');
t = t.replace(/ (letzte[nr]?|diese[nr]?|nächste[nr]?) (woche|monat|jahr)(?= )/g, ' <zeitraum>');
t = ersetze(t, 'heute|gestern|morgen|übermorgen', '<tag>');
t = t.replace(/ \d+(?= )/g, ' <zahl>');
t = ersetze(t, 'bitte|mal|doch|eigentlich|nochmal|denn|eben', '');
return t.replace(/\s+/g, ' ').trim();
}

und ergänze im createMessage zwei Felder:

    muster: muster(text),
muster_sql: sqlText(muster(text), 500),

Kopiere dieselbe Funktion muster auch in entscheiden — der Router braucht sie, um ältere Texte zu normalisieren, wenn er aus ihnen lernt.

Schritt 3 — Mehr Kontext laden​

Ändere in kontext_laden die Unterabfrage offen so, dass sie auch die Parameter liefert ('p', parameter), und ergänze drei Spalten:

  (SELECT json_object('id', id, 'f', faehigkeit, 't', text, 's', status, 'p', parameter)
FROM assistent_vorgang WHERE unterhaltung_id = '{{ unterhaltung_id }}'
AND status IN ('bestaetigung', 'rueckfrage') AND erstellt > datetime('now', '-30 minutes')
ORDER BY erstellt DESC LIMIT 1) AS offen,
(SELECT json_object('id', id, 'r', richtig) FROM assistent_lernbeispiel
WHERE muster = '{{ muster_sql }}' AND absender_id = '{{ absender_id }}' AND aktiv = 1
ORDER BY id DESC LIMIT 1) AS gelernt,
(SELECT json_group_array(json_object('id', id, 't', text, 'm', muster, 'r', richtig))
FROM (SELECT * FROM assistent_lernbeispiel WHERE absender_id = '{{ absender_id }}' AND aktiv = 1
ORDER BY id DESC LIMIT 30)) AS beispiele,
(SELECT json_object('id', id, 'f', faehigkeit, 't', text) FROM assistent_vorgang
WHERE unterhaltung_id = '{{ unterhaltung_id }}' AND erstellt > datetime('now', '-10 minutes')
AND faehigkeit IN (SELECT schluessel FROM assistent_faehigkeit WHERE schluessel <> 'hilfe')
ORDER BY erstellt DESC LIMIT 1) AS letzter
SpalteWofür
gelerntdirekter Treffer: Genau dieses Muster wurde schon einmal zugeordnet
beispieledie letzten Lernbeispiele — für die Auswahl ähnlicher Fälle im Prompt und für „was hast du gelernt?“
letzterder zuletzt ausgeführte Skill — worauf sich „nein, ich meinte …“ bezieht

Schritt 4 — Ein neuer Speicher-Agent​

Lege r_lernen an — Internal Storage Agent, sql_statement: {{ sql_lernen }}, keep_message: false, Quelle: entscheiden.

Schritt 5 — Der Router lernt​

5a — Kontext erweitern​

Ergänze im Kontext-Objekt kx von entscheiden:

    gelernt: json(r0.gelernt, null),
beispiele: json(r0.beispiele, []),
letzter: json(r0.letzter, null)

5b — Hilfsfunktionen​

Füge nach ausfuehren ein:

  var param = { zusammenfassung: '' };
o.sql_lernen = 'SELECT 1';

// Ein Lernbeispiel speichern und im Chat sagen, was gelernt wurde
function lerne(t, falsch, fk) {
o.sql_lernen = "INSERT INTO assistent_lernbeispiel (text, muster, falsch, richtig, absender_id, vorgang_id) VALUES ('" +
[sqlText(t, 500), sqlText(muster(t), 500), sqlText(falsch || '-'), sqlText(fk.s), sqlText(p.absender_id), sqlText(vid)].join("', '") + "')";
o.hinweis = 'Gemerkt: „' + String(t).slice(0, 80) + '“ heißt für mich ab jetzt **' + fk.n + '**. („vergiss das“ macht es rückgängig)';
}
// Ausführen, aber Risikostufe beachten — Gelerntes umgeht nie die Bestätigung
function sicherAusfuehren(fk) {
if ((parseInt(fk.r, 10) || 0) >= 2 && !o.bestaetigt) {
f = fk.s; risiko = parseInt(fk.r, 10) || 0; status = 'bestaetigung';
antwort = 'Ich würde Folgendes tun: **' + fk.n + '**' + NL + 'Soll ich das ausführen? Antworte mit **ja** oder **nein**.' +
(o.hinweis ? NL + NL + '_' + o.hinweis + '_' : '');
} else { ausfuehren(fk); }
}
// Rückfrage mit nummerierter Auswahl
function auswahl(optionen, einleitung, originaltext) {
var z = [einleitung];
optionen.forEach(function(k, i) { z.push('**' + (i + 1) + '** ' + k.n); });
z.push('Antworte mit der Nummer — oder schreib es genauer.');
f = optionen[0].s; status = 'rueckfrage'; antwort = z.join(NL);
param.optionen = optionen.map(function(k) { return k.s; });
o.text = originaltext; o.text_sql = sqlText(originaltext, 1000);
}
// Welcher Skill ist mit freiem Text gemeint? Einfacher Wortvergleich mit Name, Schlüssel und Beschreibung
function skillAusText(t) {
var w = String(t).toLowerCase().replace(/[^a-zäöüß0-9 ]+/g, ' ').split(/\s+/).filter(function(x) { return x.length >= 4; });
var best = null, bestPunkte = 0;
kx.katalog.forEach(function(k) {
if (k.s === 'hilfe') { return; }
var h = (k.n + ' ' + k.s.replace(/_/g, ' ') + ' ' + k.b).toLowerCase(), pk = 0;
w.forEach(function(x) { if (h.indexOf(x.slice(0, 5)) >= 0) { pk++; } });
if (pk > bestPunkte) { best = k; bestPunkte = pk; }
});
return best;
}
function andereSkills(ausser) {
return kx.katalog.filter(function(k) { return k.s !== 'hilfe' && k.s !== ausser; }).slice(0, 4);
}

5c — Rückfrage im zweiten Durchlauf mit Auswahl​

Ersetze im Block if (ki) { … } den Zweig else if (sicherheit < 0.6) { … } durch:

    } else if (sicherheit < 0.6) {
f = fk.s;
if (o.war_rueckfrage) {
status = 'abgebrochen';
antwort = 'Das habe ich leider immer noch nicht sicher verstanden. Schreib „hilfe“, dann zeige ich dir, was ich kann.';
} else {
var opt = [fk];
(ki.alternativen || []).forEach(function(s) { var a = finde(s); if (a && opt.indexOf(a) < 0 && opt.length < 3) { opt.push(a); } });
auswahl(opt, 'Das habe ich nicht sicher verstanden. Meinst du:', text);
}

und ergänze in ki_auswerten beim Objekt r.ki die Zeile alternativen: ki.alternativen || [],.

5d — Die neuen Regeln​

Füge diese Regeln nach der Regel für „nein“ bei offener Bestätigung und vor der Regel hilfe ein:

  } else if (kx.offen && kx.offen.s === 'rueckfrage' && /^\s*[1-9]\s*$/.test(text)) {
// Auswahl auf eine Rückfrage: ausführen und lernen
var optn = (json(kx.offen.p, {}).optionen || []);
var gew = finde(optn[parseInt(text, 10) - 1]);
if (!gew) { f = 'unklar'; status = 'beantwortet'; antwort = 'Diese Nummer gab es nicht zur Auswahl.'; }
else {
var urtext = String(kx.offen.t || '');
zusatz = "UPDATE assistent_vorgang SET status = 'erledigt', antwort = 'Auswahl " + parseInt(text, 10) + " in " + vid + "', aktualisiert = datetime('now') WHERE id = '" + sqlText(kx.offen.id) + "'";
o.text = urtext; o.text_sql = sqlText(urtext, 1000);
lerne(urtext, '', gew);
sicherAusfuehren(gew);
}
} else if (/^(nein|falsch)[,.!]?\s*(ich\s+)?(meinte|meine|wollte)\s+(.+)$/i.test(text)) {
// Korrektur: der letzte Skill war falsch
var rest = /^(nein|falsch)[,.!]?\s*(ich\s+)?(meinte|meine|wollte)\s+(.+)$/i.exec(text)[4];
if (!kx.letzter) { f = 'korrektur'; status = 'beantwortet'; antwort = 'Worauf bezieht sich das? Ich finde keinen Auftrag aus den letzten Minuten.'; }
else {
var richtig = skillAusText(rest);
if (richtig && richtig.s !== kx.letzter.f) {
o.text = String(kx.letzter.t || ''); o.text_sql = sqlText(o.text, 1000);
lerne(o.text, kx.letzter.f, richtig);
o.hinweis += ' Den vorigen Auftrag habe ich nicht rückgängig gemacht.';
sicherAusfuehren(richtig);
} else {
auswahl(andereSkills(kx.letzter.f), 'Was hast du gemeint?', String(kx.letzter.t || ''));
}
}
} else if (/^was hast du (gelernt|dir gemerkt)\??$/i.test(text)) {
f = 'lernen'; status = 'beantwortet';
var lb = kx.beispiele.slice(0, 10);
antwort = lb.length
? '**Was ich gelernt habe:**' + NL + lb.map(function(x) { var k = finde(x.r); return '- Nr. ' + x.id + ': „' + String(x.t).slice(0, 60) + '“ → ' + (k ? k.n : x.r); }).join(NL) + NL + '_„vergiss <Nr>“ entfernt einen Eintrag._'
: 'Ich habe noch nichts gelernt.';
} else if (/^vergiss (das|(nr\.?\s*)?(\d+))$/i.test(text)) {
f = 'lernen'; status = 'erledigt';
var nr = (/(\d+)/.exec(text) || [])[1] || (kx.beispiele[0] && kx.beispiele[0].id);
if (!nr) { status = 'beantwortet'; antwort = 'Es gibt nichts zu vergessen.'; }
else {
o.sql_lernen = "UPDATE assistent_lernbeispiel SET aktiv = 0 WHERE id = " + parseInt(nr, 10) + " AND absender_id = '" + sqlText(p.absender_id) + "'";
antwort = 'Vergessen: Eintrag Nr. ' + nr + '.';
}

Und füge direkt vor dem letzten else (KI-Erkennung) die Regel für gelernte Treffer ein:

  } else if (kx.gelernt && finde(kx.gelernt.r)) {
// 8. Gelernte Formulierung: ohne KI zuordnen
o.sql_lernen = 'UPDATE assistent_lernbeispiel SET treffer = treffer + 1 WHERE id = ' + parseInt(kx.gelernt.id, 10);
sicherAusfuehren(finde(kx.gelernt.r));

5e — Ähnliche Beispiele für die KI​

Ergänze im KI-Zweig (Regel 9), nach o.ki_text = kitext;:

    var mw = muster(kitext).split(' ');
var aehnlich = kx.beispiele.map(function(x) {
var punkte = 0; String(x.m || '').split(' ').forEach(function(w) { if (w.length >= 3 && mw.indexOf(w) >= 0) { punkte++; } });
return { x: x, punkte: punkte };
}).filter(function(e) { return e.punkte > 0; })
.sort(function(a, b) { return b.punkte - a.punkte; }).slice(0, 5);
o.ki_beispiele = aehnlich.length
? aehnlich.map(function(e) { return '- „' + String(e.x.t).slice(0, 120) + '“ → ' + e.x.r; }).join(NL)
: '(keine)';

5f — Parameter speichern und Hinweis weitergeben​

In sql_vorgang ersetze JSON.stringify({ zusammenfassung: zusammenfassung }) durch JSON.stringify(param) und setze direkt davor (vor der Zeile o.sql_vorgang = …):

  param.zusammenfassung = zusammenfassung;

Damit landen die angebotenen Optionen einer Rückfrage im Vorgang — die Auswahl-Regel liest sie von dort.

Hänge außerdem bei direkten Antworten den Hinweis an. Ersetze die Zeile if (route === 'antwort') { … } durch:

  if (route === 'antwort') {
o.antwort_text = antwort + (o.hinweis && antwort.indexOf(o.hinweis) < 0 ? NL + NL + '_' + o.hinweis + '_' : '');
o.vorgang_status = status;
}

Schritt 6 — Prompt erweitern​

Ergänze im Prompt von ki_erkennen vor „NEUE NACHRICHT“:

SO HAT DIESE PERSON ES FRÜHER GEMEINT (gelernte Zuordnungen)
{{ ki_beispiele }}

und ändere die Antwortvorgabe auf:

{"faehigkeit": "<schluessel oder unbekannt>", "sicherheit": <0 bis 1>,
"alternativen": ["<bis zu zwei weitere passende schluessel>"],
"zusammenfassung": "<ein kurzer Satz>"}

Schritt 7 — Hinweis durch die Skills reichen​

Damit „Gemerkt: …“ auch bei Antworten aus Skills erscheint:

  1. Ergänze in notiz_merken, notiz_loeschen, notiz_suchen und notiz_antwort im createMessage die Zeile hinweis: p.hinweis,.
  2. Ändere in rueckkanal die erste Zeile mit text zu:
  var text = String(p.antwort_text || '');
if (p.hinweis && text.indexOf(p.hinweis) < 0) { text += String.fromCharCode(10, 10) + '_' + p.hinweis + '_'; }

Testen​

Lösche vorher deine Notizen und Lernbeispiele nicht — die Übung baut auf echten Einträgen auf. Schicke nacheinander:

#TextErwartung
1notiz: Kaiser zurückrufen wegen SchulungNotiert (Regel, ohne KI)
2Was steht bei Kaiser an?Je nach Modell: direkt notiz_suchen oder Auswahl „1 Notizen finden · 2 …“
3afalls Auswahl: 1Notizen zu Kaiser und „Gemerkt: „Was steht bei Kaiser an?“ heißt für mich ab jetzt Notizen finden“
3bfalls direkt falsch erkannt: nein, ich meinte Notizen findenrichtige Antwort und „Gemerkt …“
3cfalls direkt richtig erkanntNimm eine knappere, mehrdeutige Formulierung wie Und Kaiser? und mach mit 3a oder 3b weiter — in den Schritten 4 und 8 dann ebenfalls diese Formulierung
4Was steht bei Kaiser an?sofort notiz_suchen — prüfe im Log: ki_erkennen lief nicht
5Was steht bei Müller an?Muster unterscheidet sich (Firma ist kein Platzhalter) → KI, aber mit dem Lernbeispiel im Prompt
6was hast du gelernt?Liste mit Nummer
7vergiss dasEintrag deaktiviert
8Was steht bei Kaiser an?wieder KI-Erkennung
Warum wird „Was steht bei Müller an?“ nicht direkt erkannt?

Weil das Muster in dieser Übung Namen noch nicht durch Platzhalter ersetzt — was steht bei kaiser an und was steht bei müller an sind verschiedene Muster. Die KI bekommt aber das ähnliche Lernbeispiel im Prompt mit und ordnet deshalb sehr wahrscheinlich richtig zu.

Die Erweiterung: Lade die Namen aus deinen Notizen oder aus dem CRM und ersetze sie in muster() durch <name> (siehe Einheit 6). Dann ergibt beides was steht bei <name> an.

Was passiert bei „nein, ich meinte Notizen löschen“?

Der Router lernt die Zuordnung, ruft aber sicherAusfuehren auf: Weil „Notizen löschen“ Risikostufe 2 hat, kommt zuerst die Bestätigungsfrage. Gelerntes ändert nie die Sicherheitsregeln.

Was du gebaut hast​

Dein Assistent hat jetzt alle Lernwege aus Einheit 6, die das Verstehen betreffen:

  • Er fragt mit Auswahl nach und lernt aus der Antwort, ohne dass die Person etwas extra tun muss.
  • Er akzeptiert Korrekturen und lernt daraus.
  • Er erkennt Gelerntes ohne KI — schneller, günstiger, verlässlicher.
  • Er gibt der KI ähnliche Fälle mit, wenn es keinen direkten Treffer gibt.
  • Er sagt, was er lernt, und lässt sich korrigieren.

Als Nächstes: Schließe ihn an einen echten Messenger an — Kurs 8.